Design - ECS

About

Construction

  • Component

    • A component is a data structure that contains information or attributes of an entity. It has no logic or behavior, only data.

    • Components are plain data that can be attached to entities to represent their attributes. Each component is an aspect of an entity, such as position, velocity, health, weapon, etc.

    • Ex :

      • A Position (with attributes x , y ) or Velocity (with attributes dx , dy ) can be components of an entity.

  • Entity

    • An entity is a unique identifier (usually a number or an ID) that represents a "thing" in the system, like a character, object, or actor in a game or simulation.

    • Entities are simple identifiers and do not have behavior or data directly attached to them. They merely aggregate components.

    • Ex :

      • In a game, a player, an enemy, a bullet, or a tree can be represented as entities.

  • System

    • A system is logic or a process that operates on one or more components of entities to modify their states or perform some operation.

    • Systems process entities that have the required components. Instead of storing data centrally, systems operate on data in a distributed way, allowing entities to behave efficiently.

    • Ex :

      • A movement system could update the position of all entities that have Position and Velocity components. An AI system could process enemy behavior (based on their health, position, etc. components).

Construction: Bevy
  • Component :

    • Just a normal Rust data type. generally scoped to a single piece of functionality.

    • Ex :

      • position, velocity, health, color, name

  • Entity :

    • A collection of components with a unique id.

    • Ex :

      • Entity1 { Name("Alice"), Position(0, 0) },

      • Entity2 { Name("Bill"), Position(10, 5) }

  • Resource :

    • A shared global piece of data.

    • Ex :

      • Asset storage, events, system state

  • System :

    • Runs logic on entities, components, and resources.

    • Ex :

      • Move system, damage system

Concepts

  • Decoupling

    • Definition :

      • It relies on composition instead of inheritance and promotes separation between data and behavior.

      • ECS promotes decoupling between data (components) and processing logic (systems).

    • Characteristics :

      • By separating data (components) and logic (systems), the architecture makes maintenance, refactoring, and extension easier. You can add new systems or components without affecting other systems or entities.

  • Immutability (optional, but common)

    • Definition :

      • Although ECS does not directly enforce immutability, some implementations commonly use immutable data structures to ensure component data is not changed directly. This helps avoid side effects and eases concurrency.

    • Characteristics :

      • In an immutable ECS implementation, when a system processes an entity's data, it produces a new version of the entity (or component) with modified data, rather than mutating existing data. This improves predictability and debugging, and helps optimize code in concurrent systems.

  • Performance and Scalability

    • Definition :

    • Characteristics :

      • The architecture enables parallelized and efficient operations, since components are stored contiguously, improving cache locality and enabling fast processing.

  • Component Storage (Data-Oriented)

    • Definition :

      • Components are stored in a data-oriented way. This means data for each component type is organized contiguously to improve memory access efficiency.

    • Characteristics :

      • This optimizes cache usage and reduces access latency, which is crucial in games and simulations that require fast real-time processing.

Implementations

  • .

    • "Sometimes I have many components that are not used, so the array becomes quite sparse. One solution would be to use Archetypes or Sparse Sets, but I don't want to lock into optimization."

  • Archetype-based systems :

    • Generally better at iterating through components.

    • Group entities with identical sets of components together in memory, allowing for contiguous memory storage and fast access patterns during iteration. It's nice in that you don't need to check whether a given entity has certain components on each iteration. Adding or removing components is generally more expensive than with sparse sets though, because you need to transfer all components for an entity from one pool to another.

  • Sparse set systems :

    • Generally better for adding / removing components.

    • Manage components individually, using a dense array for each component type and a sparse array for existence checks. It's generally better in scenarios where components are added or removed frequently because you don't need to move all components an entity has, but iteration is more expensive as it requires checks to ensure each entity has the requested components.

  • Entt and Flecs :

    • Apparently Entt and Flecs recently support both, in their own way.

    • "Entt is a sparse set ECS, flecs is an archetype".

      • I haven't read the code for either, and it's possible that one or both are doing clever things which makes everything said above irrelevant.

Examples

Applications
  • .

    • "The movement system takes the Transform and Physics components, so the entity's Transform x and y are changed using information from Physics."

  • OOP :

    • Via Chat-GPT

    // Components: defined as types (immutable types)
    type Position = { x: number, y: number };
    type Velocity = { dx: number, dy: number };
    
    // Entity: composed of components.
    type Entity = { id: number, position: Position, velocity: Velocity };
    
    // System: to update an entity's position.
    function updatePosition(entity: Entity): Entity {
        const newPosition = {
            x: entity.position.x + entity.velocity.dx,
            y: entity.position.y + entity.velocity.dy
        };
        return { ...entity, position: newPosition };
    }
    
    
    const player: Entity = { id: 1, position: { x: 0, y: 0 }, velocity: { dx: 2, dy: 3 } };
    const newPlayer = updatePosition(player);
    

Frameworks

Entt
Flecs

ECS in Godot

  • Why isn't Godot an ECS-based game engine? .

  • ECS is a design pattern commonly used in video games (although not very common in the rest of the software industry) which consists of having a base Entity (a container object) and Components that can be added upon it. Components provide data and the means to interact with the whole world. Finally, Systems work independently and act on every similar component.

  • This design became common in game engines and libraries in the early 2010s. The main appeal (besides architecture) is that component data can be placed in contiguous memory, improving cache access.

  • This is a common form of data-oriented  optimization.

  • Architecturally, ECS aims to replace inheritance , by favoring composition, similar to how interfaces  or multiple inheritance  works in OOP.

  • The key advantage in ECS is that components are dynamic  (can be added or removed at runtime).

  • Godot does composition at a higher level than in a traditional ECS.

  • One of the biggest advantages of ECS is the Systems (data-oriented) part, which allows running through a lot of similar components' data organized in linear memory.

    • This brings huge performance improvements over the way Godot works with nodes.

  • % Most (if not all) technologies that utilize ECS do it at the core engine level, by serving as the base architecture and building everything else (physics, rendering, audio, etc.) over it.

  • Godot instead keeps those subsystems separate and isolated (and they fit inside of Servers ).

    • I find this makes code simpler and easier to maintain and optimize.

  • To put it simply, nodes are just interfaces to the actual data being processed inside servers, while in ECS the actual entities are what gets processed by the systems.

    • As I understood it: Nodes seem to create a layer of abstraction over Systems, because they are very low-level.

  • In other words, Godot as an engine tries to take the burden of processing  away from the user, and instead places the focus on deciding  what to do in case of an event. This ensures users have to optimize less  in order to write much of the game code, and is part of the vision Godot conveys about what should constitute an easy to use game engine .

  • These are generally games that need to process game logic on dozens of thousands of objects, where data-oriented optimizations become necessary, as the amount of pages moved into CPU cache increases by several orders of magnitude, severely affecting performance (and battery usage on mobile devices).

    • City builders (lots of things going on).

    • Sandboxes (lots of tiny things need processing every frame).

    • Some strategy games (while not the majority, some can use thousands or tens of thousands of units at the same time).

    • Other AAA games with lots of content going on.